import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import serial
import serial.tools.list_ports
import threading
import time
import re
import math

class PendulumExperiment:
    def __init__(self, root):
        self.root = root
        self.root.title("单摆实验模拟器")
        self.root.geometry("800x600")
        self.root.resizable(True, True)
        
        # 实验数据变量
        self.port = None
        self.ser = None
        self.is_connected = False
        self.time_records = []  # 存储时间记录，用于计算周期
        self.max_records = 3    # 最多保存3条记录
        self.direction = "未知"  # 运动方向
        self.string_length = tk.StringVar(value="1.0")  # 绳长(m)
        self.gravity = tk.StringVar(value="9.8")  # 重力加速度(m/s²)
        self.period = 0.0  # 周期(s)
        self.last_detection_time = time.time()  # 最后一次检测时间
        self.current_angle = 0  # 当前摆角
        
        # 创建界面
        self.create_widgets()
        
        # 启动串口监听线程
        self.running = True
        self.listen_thread = threading.Thread(target=self.listen_serial, daemon=True)
        self.listen_thread.start()
        
        # 启动位置检查定时器
        self.check_position()
    
    def create_widgets(self):
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 串口设置区域
        serial_frame = ttk.LabelFrame(main_frame, text="串口设置", padding="10")
        serial_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(serial_frame, text="串口号:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.port_var = tk.StringVar()
        self.port_combobox = ttk.Combobox(serial_frame, textvariable=self.port_var, width=10)
        self.port_combobox.grid(row=0, column=1, padx=5, pady=5)
        
        ttk.Label(serial_frame, text="波特率:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
        self.baud_var = tk.StringVar(value="9600")
        self.baud_combobox = ttk.Combobox(serial_frame, textvariable=self.baud_var, 
                                         values=["9600", "19200", "38400", "57600", "115200"], width=10)
        self.baud_combobox.grid(row=0, column=3, padx=5, pady=5)
        
        self.refresh_btn = ttk.Button(serial_frame, text="刷新", command=self.refresh_ports)
        self.refresh_btn.grid(row=0, column=4, padx=5, pady=5)
        
        self.connect_btn = ttk.Button(serial_frame, text="连接", command=self.toggle_connection)
        self.connect_btn.grid(row=0, column=5, padx=5, pady=5)
        
        # 数据显示区域
        data_frame = ttk.LabelFrame(main_frame, text="实验数据", padding="10")
        data_frame.pack(fill=tk.X, pady=5)
        
        # 光电门时间
        ttk.Label(data_frame, text="光电门1时间(us):").grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)
        self.get1_label = ttk.Label(data_frame, text="0.0")
        self.get1_label.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W)
        
        ttk.Label(data_frame, text="光电门2时间(us):").grid(row=0, column=2, padx=10, pady=5, sticky=tk.W)
        self.get2_label = ttk.Label(data_frame, text="0.0")
        self.get2_label.grid(row=0, column=3, padx=10, pady=5, sticky=tk.W)
        
        # 运动方向
        ttk.Label(data_frame, text="小球运动方向:").grid(row=1, column=0, padx=10, pady=5, sticky=tk.W)
        self.direction_label = ttk.Label(data_frame, text="未知")
        self.direction_label.grid(row=1, column=1, padx=10, pady=5, sticky=tk.W)
        
        # 周期
        ttk.Label(data_frame, text="周期(s):").grid(row=1, column=2, padx=10, pady=5, sticky=tk.W)
        self.period_label = ttk.Label(data_frame, text="0.0")
        self.period_label.grid(row=1, column=3, padx=10, pady=5, sticky=tk.W)
        
        # 时间记录显示
        ttk.Label(data_frame, text="时间记录:").grid(row=2, column=0, padx=10, pady=5, sticky=tk.W)
        self.records_label = ttk.Label(data_frame, text="无数据")
        self.records_label.grid(row=2, column=1, padx=10, pady=5, sticky=tk.W)
        
        # 计算区域
        calc_frame = ttk.LabelFrame(main_frame, text="参数计算", padding="10")
        calc_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(calc_frame, text="绳长(m):").grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)
        ttk.Entry(calc_frame, textvariable=self.string_length, width=15).grid(row=0, column=1, padx=10, pady=5)
        
        ttk.Label(calc_frame, text="重力加速度(m/s²):").grid(row=0, column=2, padx=10, pady=5, sticky=tk.W)
        ttk.Entry(calc_frame, textvariable=self.gravity, width=15).grid(row=0, column=3, padx=10, pady=5)
        
        self.calc_g_btn = ttk.Button(calc_frame, text="由绳长计算重力加速度", command=self.calculate_gravity)
        self.calc_g_btn.grid(row=0, column=4, padx=10, pady=5)
        
        self.calc_l_btn = ttk.Button(calc_frame, text="由重力加速度计算绳长", command=self.calculate_length)
        self.calc_l_btn.grid(row=0, column=5, padx=10, pady=5)
        
        # 单摆模拟区域
        sim_frame = ttk.LabelFrame(main_frame, text="单摆模拟", padding="10")
        sim_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        self.canvas = tk.Canvas(sim_frame, bg="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # 初始绘制单摆（垂直位置）
        self.draw_pendulum(0)
        
        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="日志", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        self.log_text = scrolledtext.ScrolledText(log_frame, height=5, wrap=tk.WORD)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        self.log_text.config(state=tk.DISABLED)
        
        # 刷新端口列表
        self.refresh_ports()
    
    def refresh_ports(self):
        """刷新可用串口号列表"""
        ports = serial.tools.list_ports.comports()
        self.port_combobox['values'] = [port.device for port in ports]
        if ports:
            self.port_combobox.current(0)
    
    def toggle_connection(self):
        """切换串口连接状态"""
        if self.is_connected:
            self.disconnect()
        else:
            self.connect()
    
    def connect(self):
        """连接到指定串口"""
        try:
            port = self.port_var.get()
            baudrate = int(self.baud_var.get())
            
            if not port:
                messagebox.showerror("错误", "请选择串口号")
                return
            
            self.ser = serial.Serial(port, baudrate, timeout=1)
            self.is_connected = True
            self.connect_btn.config(text="断开")
            self.log("已连接到串口: " + port)
        except Exception as e:
            messagebox.showerror("连接错误", str(e))
            self.log("连接失败: " + str(e))
    
    def disconnect(self):
        """断开串口连接"""
        if self.ser and self.ser.is_open:
            self.ser.close()
        self.is_connected = False
        self.connect_btn.config(text="连接")
        self.log("已断开串口连接")
    
    def listen_serial(self):
        """监听串口数据的线程函数"""
        while self.running:
            if self.is_connected and self.ser and self.ser.is_open:
                try:
                    data = self.ser.readline().decode().strip()
                    if data:
                        self.log(f"收到数据: {data}")
                        self.process_data(data)
                        self.last_detection_time = time.time()  # 更新最后检测时间
                except Exception as e:
                    self.log(f"串口错误: {str(e)}")
                    self.is_connected = False
                    self.root.after(0, lambda: self.connect_btn.config(text="连接"))
            time.sleep(0.01)
    
    def check_position(self):
        """检查是否需要将单摆重置到垂直位置"""
        if time.time() - self.last_detection_time > 3:  # 3秒未检测到数据
            if self.current_angle != 0:
                self.current_angle = 0
                self.root.after(0, lambda: self.draw_pendulum(0))
                self.log("3秒未检测到数据，单摆已重置到垂直位置")
        
        # 继续定时检查
        self.root.after(1000, self.check_position)  # 每1秒检查一次
    
    def process_data(self, data):
        """处理收到的串口数据"""
        # 匹配格式 get1:XXX,get2:XXX
        pattern = r'get1:(\d+),get2:(\d+)'
        match = re.match(pattern, data)
        
        if match:
            try:
                new_get1 = float(match.group(1))
                new_get2 = float(match.group(2))
                
                # 更新时间显示
                self.root.after(0, lambda: self.get1_label.config(text=str(new_get1)))
                self.root.after(0, lambda: self.get2_label.config(text=str(new_get2)))
                
                # 判断运动方向
                direction = "从光电门1到光电门2" if new_get1 < new_get2 else "从光电门2到光电门1"
                self.direction = direction
                self.root.after(0, lambda: self.direction_label.config(text=direction))
                
                # 保存时间记录（使用两个光电门的平均时间）
                avg_time = (new_get1 + new_get2) / 2
                self.time_records.append(avg_time)
                # 只保留最近的3条记录
                if len(self.time_records) > self.max_records:
                    self.time_records.pop(0)
                
                # 更新记录显示
                records_text = ", ".join([f"{t:.1f}" for t in self.time_records])
                self.root.after(0, lambda: self.records_label.config(text=records_text))
                
                # 计算周期（使用第一次和第三次的时间差）
                if len(self.time_records) >= 3:
                    time_diff = (self.time_records[2] - self.time_records[0]) / 1000000  # 转换为秒
                    self.period = time_diff
                    self.root.after(0, lambda: self.period_label.config(text=f"{self.period:.6f}"))
                    self.log(f"计算周期: {self.period:.6f} s (使用第1和第3次数据)")
                
                # 更新单摆模拟（固定幅度，只体现方向）
                angle = 30 if direction == "从光电门1到光电门2" else -30
                self.current_angle = angle
                self.root.after(0, lambda: self.draw_pendulum(angle))
                
            except ValueError as e:
                self.log(f"数据解析错误: {str(e)}")
    
    def draw_pendulum(self, angle_deg):
        """在画布上绘制单摆，光电门位于垂直位置两侧"""
        self.canvas.delete("all")
        
        # 画布尺寸
        width = self.canvas.winfo_width() or 600
        height = self.canvas.winfo_height() or 300
        
        # 摆长 (根据画布高度调整)
        pendulum_length = min(width, height) * 0.4
        
        # 悬挂点
        pivot_x = width / 2
        pivot_y = height / 3
        
        # 计算摆球位置
        angle_rad = math.radians(angle_deg)
        ball_x = pivot_x + pendulum_length * math.sin(angle_rad)
        ball_y = pivot_y + pendulum_length * math.cos(angle_rad)
        
        # 绘制悬挂点
        self.canvas.create_oval(pivot_x - 5, pivot_y - 5, pivot_x + 5, pivot_y + 5, fill="black")
        
        # 绘制摆线
        self.canvas.create_line(pivot_x, pivot_y, ball_x, ball_y, width=2)
        
        # 绘制摆球
        ball_radius = 15
        self.canvas.create_oval(ball_x - ball_radius, ball_y - ball_radius,
                               ball_x + ball_radius, ball_y + ball_radius, fill="red")
        
        # 绘制平衡位置参考线（虚线）
        self.canvas.create_line(pivot_x, pivot_y, pivot_x, pivot_y + pendulum_length, 
                              dash=(2, 2), fill="gray")
        
        # 调整光电门位置：位于绳子垂直时（平衡位置）的左右两侧
        # 垂直位置时摆球的x坐标（平衡位置）
        balance_x = pivot_x
        # 光电门距离平衡位置的水平距离（约为摆长的1/4）
        gate_offset = pendulum_length * 0.25
        
        gate1_x = balance_x - gate_offset  # 平衡位置左侧
        gate2_x = balance_x + gate_offset  # 平衡位置右侧
        gate_y = pivot_y + pendulum_length * 0.5  # 垂直方向位置
        
        # 光电门1（左侧）
        self.canvas.create_line(gate1_x - 10, gate_y - 20, gate1_x - 10, gate_y + 20, width=2, dash=(4, 2))
        self.canvas.create_line(gate1_x + 10, gate_y - 20, gate1_x + 10, gate_y + 20, width=2, dash=(4, 2))
        self.canvas.create_text(gate1_x, gate_y - 30, text="光电门1")
        
        # 光电门2（右侧）
        self.canvas.create_line(gate2_x - 10, gate_y - 20, gate2_x - 10, gate_y + 20, width=2, dash=(4, 2))
        self.canvas.create_line(gate2_x + 10, gate_y - 20, gate2_x + 10, gate_y + 20, width=2, dash=(4, 2))
        self.canvas.create_text(gate2_x, gate_y - 30, text="光电门2")
    
    def calculate_gravity(self):
        """根据绳长和周期计算重力加速度"""
        try:
            l = float(self.string_length.get())
            if self.period <= 0 or len(self.time_records) < 3:
                messagebox.showwarning("警告", "请先获取至少3组时间数据")
                return
                
            # 单摆周期公式: T = 2π√(l/g) → g = 4π²l/T²
            g = (4 * math.pi **2 * l) / (self.period** 2)
            self.gravity.set(f"{g:.4f}")
            self.log(f"计算得到重力加速度: {g:.4f} m/s²")
        except ValueError:
            messagebox.showerror("错误", "请输入有效的绳长数值")
    
    def calculate_length(self):
        """根据重力加速度和周期计算绳长"""
        try:
            g = float(self.gravity.get())
            if self.period <= 0 or len(self.time_records) < 3:
                messagebox.showwarning("警告", "请先获取至少3组时间数据")
                return
                
            # 单摆周期公式: T = 2π√(l/g) → l = gT²/(4π²)
            l = (g * self.period **2) / (4 * math.pi** 2)
            self.string_length.set(f"{l:.4f}")
            self.log(f"计算得到绳长: {l:.4f} m")
        except ValueError:
            messagebox.showerror("错误", "请输入有效的重力加速度数值")
    
    def log(self, message):
        """添加日志信息"""
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)
    
    def on_close(self):
        """关闭窗口时的处理"""
        self.running = False
        self.disconnect()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = PendulumExperiment(root)
    root.protocol("WM_DELETE_WINDOW", app.on_close)
    root.mainloop()